﻿namespace Ngs.Data.Objects
{
    #region Using Directives

    using System;
    using System.Collections.Generic;
    using System.Data.Objects;
    using System.Data.Objects.DataClasses;
    using System.Linq;
    using Ngs.Data;
    using Ngs.Extensions;
    #endregion

    /// <summary>
    /// Object used to persist Domain objects to Microsoft Entity Framework objects.
    /// </summary>
    /// <typeparam name="T">T must be a reference type implement IIntKey.</typeparam>
    /// <typeparam name="K">K must be of type EntityObject, implement IIntKey and a () constructor.</typeparam>
    public class EntityStorage<T, K> : IStorageStrategy<T>
        where T : class, IDataEntity, new()
        where K : EntityObject, IItem, new()
    {
        #region Fields

        protected string EntityName;
        protected ObjectContext EntityContext;
        protected ObjectMap<T, K> Mapping;
        protected IList<IQueryable> subQueries;
        #endregion

        #region Constructor
        public EntityStorage(ObjectContext entityContext, ObjectMap<T, K> mapping)
            : this(entityContext, mapping, null)
        {
        }

        public EntityStorage(ObjectContext entityContext, ObjectMap<T, K> mapping, params IQueryable[] subQueriables)
        {
            ExceptionFactory.ThrowIfArgumentNull(entityContext);
            ExceptionFactory.ThrowIfArgumentNull(mapping);

            this.EntityName = !string.IsNullOrEmpty(mapping.EntityName) ? mapping.EntityName : typeof(K).Name;
            this.EntityContext = entityContext;
            this.Mapping = mapping;

            if (subQueriables != null)
            {
                this.subQueries = new List<IQueryable>(subQueriables);
            }
        }

        #endregion

        #region CRUD Methods

        public T Create(T entity)
        {
            K efEntity = new K();
            this.Mapping.ObjectToEntity(entity, efEntity);
            this.EntityContext.AddObject(this.EntityName, efEntity);
            this.EntityContext.SaveChanges();
            entity.Id = efEntity.EntityKey.ToInt32();

            return entity;
        }

        public void Delete(T entity)
        {
            K k = Attach(entity);
            Mapping.ObjectToEntity(entity, k);
            EntityContext.DeleteObject(k);
        }

        public IDataQuery<T> Query()
        {
            ExceptionFactory.ThrowInvalidOperation(this.Mapping == null);
            Converter<K, T> converter = this.Mapping.ConverToObject;
            return new QueryProxy<T, K>(CreateObjectQuery(), converter, subQueries);
        }

        public void Save(T entity)
        {
            K k = Attach(entity);
            this.Mapping.ObjectToEntity(entity, k);
        }
        #endregion

        protected ObjectQuery<K> CreateObjectQuery()
        {
            return this.EntityContext.CreateQuery<K>(this.EntityName);
        }

        /// <summary>
        /// Attaches change tracking.
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        protected virtual K Attach(T entity)
        {
            return this.CreateObjectQuery().Where(c => c.Id == entity.Id).First();
        }

        /// <summary>
        /// Removes change tracking and reference held by ObjectContext(EF).
        /// </summary>
        /// <param name="entity"></param>
        protected virtual void Dettach(K entity)
        {
            this.EntityContext.DeleteObject(entity);
        }

        #region IStorageStrategy Members

        IQueryable IStorageStrategy.Queryable()
        {
            return this.Query();
        }

        object IStorageStrategy.Create(object resource)
        {
            return this.Create(resource.AsGenericType<T>());
        }

        void IStorageStrategy.Delete(object resource)
        {
            this.Delete(resource.AsGenericType<T>());
        }

        void IStorageStrategy.Save(object resource)
        {
            this.Save(resource.AsGenericType<T>());
        }

        #endregion
    }
}
